// ============================================================================
// FileName: ClientUserAgent.cs
//
// Description: SIP user agent for client transactions/dialogues.
//
// Author(s):
// Aaron Clauson
//
// History:
// 07 Feb 2006	Aaron Clauson	Created.
//
// License:
// Aaron Clauson
// ============================================================================

using System;
using System.Collections;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using BlueFace.Sys;
using BlueFace.Sys.Net;
using BlueFace.VoIP.Authentication;
using BlueFace.VoIP.Net;
using BlueFace.VoIP.Net.App;
using BlueFace.VoIP.Net.App.SessionDescriptors;
using BlueFace.VoIP.Net.SIP;
using Heijden.DNS;
using log4net;

namespace BlueFace.VoIP.UserAgents
{
	public delegate void UserAgentFinalResponseDelegate(SIPTransaction transaction);
    public delegate void UserAgentRequestTimedOutDelegate(SIPTransaction transaction);
    public delegate void UserAgentCallHungupDelegate();
    public delegate void UserAgentCallRetransmitDelegate(int retransmits);
    
    public class ClientUserAgent
	{	
		private ILog logger = AppState.logger;

        private SIPTransport m_sipTransport;

        private UACInviteTransaction m_clientTransaction;
        private SIPURI m_dstURI;
        private string m_username;
        private string m_password;
        private SIPToHeader m_to;
        private SIPFromHeader m_from;
        private string m_callId;
        private string m_contentType;
        private string m_content;
        private IPEndPoint m_sendFromEndPoint;
        private IPEndPoint m_dstEndPoint;

        private bool m_authenticatedCallAttempted = false;

        public event UserAgentFinalResponseDelegate CallFinalResponseReceived;
        public event UserAgentRequestTimedOutDelegate CallTimedOut;
        public event UserAgentCallHungupDelegate CallHungup;
        //public event UserAgentCallRetransmitDelegate CallRetransmit;

        public string Owner;
        public SIPDialogue CallDialogue;
        public SIPURI ServerContactURI;
        public SIPTransactionStatesEnum CallState
        {
            get { return m_clientTransaction.TransactionState; }
        }
        public UACInviteTransaction ClientTransaction
        {
            get { return m_clientTransaction; }
        }
        public SDP LocalSDP;
        public SDP RemoteSDP;

    	public ClientUserAgent(SIPTransport sipTransport, string owner)
		{
            m_sipTransport = sipTransport;
            Owner = owner;
         }

        public void Call(SIPCallDescriptor callDescriptor, string contentType, string inviteBody)
        {
            try
            {
                m_dstURI = SIPURI.ParseSIPURI(callDescriptor.Uri);
                m_sendFromEndPoint = m_sipTransport.GetTransportContact(null);
                m_to = SIPToHeader.ParseToHeader(callDescriptor.To);
                m_from = SIPFromHeader.ParseFromHeader(callDescriptor.From);
                m_from.FromTag = CallProperties.CreateNewTag();
                m_callId = CallProperties.CreateNewCallId();
                m_username = callDescriptor.Username;
                m_password = callDescriptor.Password;
                m_contentType = contentType;
                m_content = inviteBody;
                string dstEndPoint = (callDescriptor.Host != null) ? callDescriptor.Host : m_dstURI.Host;

                if (IPSocket.IsIPSocket(dstEndPoint))
                {
                    string ipAddress = null;
                    int port = 0;
                    IPSocket.ParseSocketString(dstEndPoint, out ipAddress, out port);

                    port = (port == 0) ? SIPConstants.DEFAULT_SIP_PORT : port;

                    m_dstEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
                }
                else
                {
                    DNSResponse response = DNSManager.Lookup(dstEndPoint, DNSQType.A, 5000, null);
                    if (response != null && response.Answers.Count > 0)
                    {
                        m_dstEndPoint = new IPEndPoint(response.RecordsA[0].Address, SIPConstants.DEFAULT_SIP_PORT);
                    }
                    else
                    {
                        throw new ApplicationException("Could not resolve " + dstEndPoint + " in ClientUserAgent.");
                    }
                }

                SendCall(m_sendFromEndPoint, m_dstEndPoint, m_dstURI, m_to, m_from, 1, m_callId, m_contentType, m_content, null);
            }
            catch (Exception excp)
            {
                logger.Error("Exception Call (ClientUserAgent). " + excp.Message);
                throw excp;
            }
        }

        public void Call(SIPURI dstURI, string contentType, string inviteBody)
        {
            Call(m_sipTransport.GetTransportContact(null), dstURI, contentType, inviteBody);
        }

        public void Call(IPEndPoint srcSIPSocket, SIPURI dstURI, string contentType, string inviteBody)
        {
            try
            {
                m_dstURI = dstURI;
                
                IPEndPoint localSIPEndPoint = m_sipTransport.GetTransportContact(null);
                m_to = new SIPToHeader(null, m_dstURI, null);
                m_from = new SIPFromHeader(null, new SIPURI(SIPConstants.SIP_DEFAULT_USERNAME, localSIPEndPoint.ToString(), null), CallProperties.CreateNewTag());
                m_callId = CallProperties.CreateNewCallId();
                m_contentType = contentType;
                m_content = inviteBody;

                SendCall(srcSIPSocket, null, m_dstURI, m_to, m_from, 1, m_callId, m_contentType, m_content, null);
            }
            catch (Exception excp)
            {
                logger.Error("Exception Call (ClientUserAgent). " + excp.Message);
                throw excp;
            }
        }

        public void Call(SIPURI dstURI, SIPFromHeader from, string username, string password, string contentType, string inviteBody)
        {
            Call(m_sipTransport.GetTransportContact(null), dstURI, from, username, password, contentType, inviteBody);
        }

        public void Call(IPEndPoint srcSIPSocket, SIPURI dstURI, SIPFromHeader from, string username, string password, string contentType, string inviteBody)
        {
            try
            {
                m_dstURI = dstURI;
                m_from = from;
                m_username = username;
                m_password = password;
                m_callId = CallProperties.CreateNewCallId();
                m_contentType = contentType;
                m_content = inviteBody;

                m_to = new SIPToHeader(null, m_dstURI, null);

                SendCall(srcSIPSocket, null, m_dstURI, m_to, m_from, 1, m_callId, m_contentType, m_content, null);
            }
            catch (Exception excp)
            {
                //logger.Error("Exception Call (ClientUserAgent). " + excp.Message);
                throw excp;
            }
        }

        private void SendCall(IPEndPoint srcEndPoint, IPEndPoint dstEndPoint, SIPURI dstURI, SIPToHeader to, SIPFromHeader from, int cseq, string callId, string contentType, string content, SIPAuthenticationHeader authHeader)
        {
            try
            {
                m_sendFromEndPoint = srcEndPoint;
                LocalSDP = SDP.ParseSDPDescription(content);
                SIPRequest inviteRequest = GetInviteRequest(dstURI, srcEndPoint, to, from, cseq, callId, contentType, content, authHeader);

                m_clientTransaction = m_sipTransport.CreateUACTransaction(inviteRequest, inviteRequest.GetRequestEndPoint(), srcEndPoint, dstURI.Protocol);
                m_clientTransaction.CDR.Owner = Owner;
                m_clientTransaction.SendFromEndPoint = srcEndPoint;
                m_clientTransaction.UACInviteTransactionInformationResponseReceived += new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionInformationResponseReceived);
                m_clientTransaction.UACInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionFinalResponseReceived);
                //m_clientTransaction.TransactionRequestRetransmit += new SIPTransactionRequestRetransmitDelegate(m_clientTransaction_TransactionRequestRetransmit);

                dstEndPoint = (dstEndPoint == null) ? inviteRequest.GetRequestEndPoint() : dstEndPoint;
                m_clientTransaction.SendInviteRequest(dstEndPoint, inviteRequest);
            }
            catch (Exception excp)
            {
                logger.Error("Exception SendCall. " + excp.Message);
            }
        }

        public void Cancel()
        {
            try
            {
                throw new NotImplementedException();
            }
            catch (Exception excp)
            {
                //logger.Error("Exception Call (ClientUserAgent). " + excp.Message);
                throw excp;
            }
        }

        public void Hangup()
        {
            try
            {
                if (CallDialogue != null && CallDialogue.DialogueId != null)
                {
                    SIPRequest byeRequest = GetByeRequest(ServerContactURI, m_clientTransaction.TransactionRequest.Header, m_sipTransport.GetTransportContact(null));

                    logger.Debug("ClientUserAgent, sending BYE to " + byeRequest.URI.ToString() + ".");

                    SIPNonInviteTransaction byeTransaction = m_sipTransport.CreateNonInviteTransaction(byeRequest, byeRequest.GetRequestEndPoint(), m_sipTransport.GetTransportContact(null), m_clientTransaction.Protocol);
                    byeTransaction.SendReliableRequest();
                }
                else
                {
                    logger.Warn("Cannot hangup call as ClientUA call dialogue does not exist.");
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception Hangup. " + excp.Message);
            }
        }

        public void Reinvite(string ipAddress, int port)
        {
            SIPRequest reinviteRequest = m_clientTransaction.TransactionRequest;
            reinviteRequest.Header.To.ToTag = m_clientTransaction.TransactionFinalResponse.Header.To.ToTag;
            reinviteRequest.Header.CSeq = reinviteRequest.Header.CSeq + 1;
            reinviteRequest.Header.Via.TopViaHeader.Branch = CallProperties.CreateBranchId();
            LocalSDP.Media[0].ConnectionAddress = ipAddress;
            LocalSDP.Media[0].Port = port;
            reinviteRequest.Body = LocalSDP.ToString();
            reinviteRequest.Header.ContentLength = reinviteRequest.Body.Length;

            logger.Debug("Sending reinvite to " + reinviteRequest.GetRequestEndPoint() + " for " + reinviteRequest.URI.ToString());

            m_clientTransaction = m_sipTransport.CreateUACTransaction(reinviteRequest, reinviteRequest.GetRequestEndPoint(), m_sendFromEndPoint, m_clientTransaction.Protocol);
            m_clientTransaction.CDR.Owner = Owner;
            m_clientTransaction.SendFromEndPoint = m_sendFromEndPoint;
            m_clientTransaction.UACInviteTransactionInformationResponseReceived += new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionInformationResponseReceived);
            m_clientTransaction.UACInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionFinalResponseReceived);

            m_clientTransaction.SendInviteRequest(reinviteRequest.GetRequestEndPoint(), reinviteRequest);
        }

        private void m_clientTransaction_TransactionRequestTimedOut(SIPTransaction sipTransaction)
        {
            logger.Debug("Client call to " + m_dstURI.ToString() + " timed out.");

            if (CallTimedOut != null)
            {
                CallTimedOut(m_clientTransaction);
            }
        }

        private void m_clientTransaction_TransactionInformationResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            logger.Debug("Information response " + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + " for " + m_dstURI.ToString() + ".");
        }

        private void m_clientTransaction_TransactionFinalResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            logger.Debug("Final response " + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + " for " + m_dstURI.ToString() + ".");

            m_clientTransaction.UACInviteTransactionInformationResponseReceived -= new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionInformationResponseReceived);
            m_clientTransaction.UACInviteTransactionFinalResponseReceived -= new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionFinalResponseReceived);

            ServerContactURI = m_dstURI;
            if (sipResponse.Header.Contact != null && sipResponse.Header.Contact.Count > 0)
            {
                ServerContactURI = sipResponse.Header.Contact[0].ContactURI;
            }

            if (sipResponse.StatusCode >= 200 && sipResponse.StatusCode < 299)
            {
                RemoteSDP = SDP.ParseSDPDescription(sipResponse.Body);
                
                SIPRouteSet serverRouteSet = (sipResponse.Header.RecordRoutes != null) ? sipResponse.Header.RecordRoutes.Reversed() : null;
                CallDialogue = new SIPDialogue(
                            m_sipTransport,
                            sipTransaction.TransactionRequest.Header.CallId,
                            serverRouteSet,
                            sipTransaction.TransactionRequest.Header.From.FromUserField,
                            sipTransaction.TransactionRequest.Header.CSeq,
                            sipTransaction.TransactionFinalResponse.Header.To.ToUserField,
                            -1,
                            sipTransaction.TransactionFinalResponse.Header.Contact[0].ContactURI,
                            sipTransaction.TransactionRequest.Header.From.FromTag,
                            sipTransaction.TransactionFinalResponse.Header.To.ToTag,
                            localEndPoint,
                            ((UACInviteTransaction)sipTransaction).CDR,
                            Owner,
                            null,
                            LocalSDP.ToString());

                if (CallFinalResponseReceived != null)
                {
                    CallFinalResponseReceived(m_clientTransaction);
                }
            }
            else if ((sipResponse.StatusCode == 407 || sipResponse.StatusCode == 401) && m_username != null && m_password != null && !m_authenticatedCallAttempted)
            {
                logger.Debug("Sending authenticated INVITE request to " + m_dstURI.ToString() + ".");

                m_authenticatedCallAttempted = true;

                string nonce = sipResponse.Header.AuthenticationHeader.AuthRequest.Nonce;
                string realm = sipResponse.Header.AuthenticationHeader.AuthRequest.Realm;
                AuthorizationRequest authRequest = new AuthorizationRequest(realm, m_username, m_password, m_dstURI.ToString(), nonce, "INVITE");

                SIPAuthenticationHeader authHeader = new SIPAuthenticationHeader(authRequest);
                authHeader.AuthRequest.Response = authRequest.Digest;
                int cseq = sipResponse.Header.CSeq + 1;
                m_from.FromTag = CallProperties.CreateNewTag();
                SIPRequest authInviteRequest = GetInviteRequest(m_dstURI, sipTransaction.SendFromEndPoint, m_to, m_from, cseq, m_callId, m_contentType, m_content, authHeader);

                m_clientTransaction = m_sipTransport.CreateUACTransaction(authInviteRequest, authInviteRequest.GetRequestEndPoint(), m_sendFromEndPoint, SIPProtocolsEnum.UDP);
                m_clientTransaction.CDR.Owner = Owner;
                m_clientTransaction.SendFromEndPoint = m_sendFromEndPoint;
                m_clientTransaction.UACInviteTransactionInformationResponseReceived+= new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionInformationResponseReceived);
                m_clientTransaction.UACInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(m_clientTransaction_TransactionFinalResponseReceived);
                m_clientTransaction.SendInviteRequest(authInviteRequest.GetRequestEndPoint(), authInviteRequest);
            }
            else
            {
                if (CallFinalResponseReceived != null)
                {
                    CallFinalResponseReceived(m_clientTransaction);
                }
            }
        }

        private void DialogueRemoved(SIPDialogue sipDialogue)
        {
            try
            {
                if(CallHungup != null)
                {
                    CallHungup();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception DialogueRemoved. " + excp.Message);
            }
        }

        private SIPRequest GetInviteRequest(SIPURI dstURI, IPEndPoint localEndPoint, SIPToHeader to, SIPFromHeader from, int cseq, string callId, string contentType, string inviteBody, SIPAuthenticationHeader authHeader)
        {
            SIPRequest inviteRequest = new SIPRequest(SIPMethodsEnum.INVITE, dstURI);

            SIPHeader inviteHeader = new SIPHeader(from, to, cseq, callId);

            //inviteHeader.From.FromTag = CallProperties.CreateNewTag();
            inviteHeader.Contact = SIPContactHeader.ParseContactHeader("sip:" + IPSocket.GetSocketString(localEndPoint));
            inviteHeader.CSeqMethod = SIPMethodsEnum.INVITE;
            inviteRequest.Header = inviteHeader;

            SIPViaHeader viaHeader = new SIPViaHeader(localEndPoint.Address.ToString(), localEndPoint.Port, CallProperties.CreateBranchId());
            inviteRequest.Header.Via.PushViaHeader(viaHeader);

            if (authHeader != null)
            {
                inviteRequest.Header.AuthenticationHeader = authHeader;
                inviteRequest.Header.ProxyAuthorization = true; // This is what Asterisk uses.
            }

            inviteRequest.Body = inviteBody;

            if (inviteBody != null)
            {
                inviteRequest.Header.ContentLength = inviteBody.Length;
            }

            if (contentType == null || contentType.Trim().Length == 0)
            {
                inviteRequest.Header.ContentType = SessionDescriptionTypes.SDP_CONTENTTYPE;
            }
            else
            {
                inviteRequest.Header.ContentType = contentType;
            }

            return inviteRequest;
        }

        private SIPRequest GetByeRequest(SIPURI serverContact, SIPHeader sipHeader, IPEndPoint localEndPoint)
        {
            SIPRequest byeRequest = new SIPRequest(SIPMethodsEnum.BYE, serverContact);

            int cseq = sipHeader.CSeq + 1;
            SIPHeader byeHeader = new SIPHeader(sipHeader.From, sipHeader.To, cseq, CallDialogue.CallId);

            byeHeader.From.FromTag = CallProperties.CreateNewTag();
            byeHeader.Contact = SIPContactHeader.ParseContactHeader("sip:" + IPSocket.GetSocketString(localEndPoint));
            byeHeader.CSeqMethod = SIPMethodsEnum.BYE;
            byeRequest.Header = byeHeader;

            SIPViaHeader viaHeader = new SIPViaHeader(localEndPoint.Address.ToString(), localEndPoint.Port, CallProperties.CreateBranchId());
            byeRequest.Header.Via.PushViaHeader(viaHeader);

            return byeRequest;
        }
	}
}
